﻿#region Copyright and license information
// Copyright 2001-2009 Stephen Colebourne
// Copyright 2009-2011 Jon Skeet
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion

using System.Linq;
using NodaTime.Text;
using NodaTime.Utility;
using System;
using System.Collections.Generic;
using System.Xml;

namespace NodaTime.Xml
{
    /// <summary>
    ///   Allows conversion between Noda types and XML strings and vice-versa.  All values are locale independent.
    /// </summary>
    /// <remarks>
    ///  The <b>NodaXmlConvert</b> methods are used to convert data between strings and strongly-typed Noda data types. Methods are locale independent. 
    ///  This means that they do not take into account any locale settings when doing conversion.
    /// <para>
    ///  Most Noda types are represented as conformant XML Schema data types, as defined in 
    ///  <see href="http://www.w3.org/TR/xmlschema-2/#isoformats">Appendix D of XML Schema Part 2: Datatypes Second Edition</see>. These types are
    ///  basically derived from the <see href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</see> extended format.  
    ///  This allows non-Noda aware applications to process the XML data generated by Noda applications.
    ///  In fact, most types in the <see cref="NodaTimeSchema"/> are defined as a simple content type restricting a pre-existing XML Schema date/time type. 
    /// </para>
    /// <para>
    ///  Variations with ISO 8601 and XML Schema occur when the <see cref="CalendarSystem"/> is
    ///  not <see cref="CalendarSystem.Iso"/> or Noda provides extra capabilities not defined by ISO 8601, such as a 
    ///  <see cref="Period"/> with positive and negative components.  These special cases cause the <b>ToString()</b> methods 
    ///  to throw an <see cref="ArgumentException"/>.  The <b>ToStringRelaxed()</b> methods can be used to format these special cases.
    /// </para>
    /// <para>
    ///  <b>NodaXmlConvert</b> is based on the ideas of the <see cref="XmlConvert"/> class.  If a XML string cannot be parsed, then
    ///  a <see cref="FormatException"/> is <c>thrown</c>, not <see cref="NodaTime.Text.UnparsableValueException"/>.
    /// </para>
    /// <para>
    ///  The following table lists the Noda data types and the XML strings that are returned using the <see cref="NodaTimeSchema.Schema">Noda Time Schema (XSD)</see>
    ///  mappings.
    /// </para>
    /// <para>
    ///  <list type="table">
    ///   <listheader><term>NodaTime type</term><description>String returned</description></listheader>
    /// <item><term><see cref='Duration'/></term><description>PT2H</description></item>
    /// <item><term><see cref='Instant'/></term><description>2012-10-31T07:49:00Z</description></item>
    /// <item><term><see cref='Interval'/></term><description>2012-10-31T07:49:00Z/2012-10-31T09:49:00Z</description></item>
    /// <item><term><see cref='LocalDate'/></term><description>2012-10-31</description></item>
    /// <item><term><see cref='LocalDateTime'/></term><description>2012-10-31T14:49:00</description></item>
    /// <item><term><see cref='LocalTime'/></term><description>14:49:00</description></item>
    /// <item><term><see cref='Offset'/></term><description>+07:00</description></item>
    /// <item><term><see cref='OffsetDateTime'/></term><description>2012-10-31T14:49:00+07:00</description></item>
    /// <item><term><see cref='Period'/></term><description>P3M2DT1H</description></item>
    ///  </list>
    /// </para>
    /// <para>
    ///  Relaxed XML data type rules. 
    /// <list type="bullet">
    /// <item>The calendar id followed by ':' is pre-pended to the extended ISO 8601 form.</item>
    /// <item>Negative <see cref="Period"/> components are allowed.</item>
    /// </list>
    /// 
    ///  <list type="table">
    ///   <listheader><term>NodaTime type</term><description>String returned</description></listheader>
    /// <item><term><see cref='Interval'/></term><description>ISO:2012-10-31T07:49:00Z/ISO:2012-10-31T09:49:00Z</description></item>
    /// <item><term><see cref='LocalDate'/></term><description>ISO:2012-10-31</description></item>
    /// <item><term><see cref='LocalDateTime'/></term><description>ISO:2012-10-31T14:49:00</description></item>
    /// <item><term><see cref='OffsetDateTime'/></term><description>ISO:2012-10-31T14:49:00+07:00</description></item>
    /// <item><term><see cref='Period'/></term><description>P-3M2DT-1H</description></item>
    ///  </list>
    /// </para>
    /// <para>
    ///   The <see cref="ZonedDateTime"/> has no corresponding XML data type and always returns a string like
    ///   "ISO:2012-10-31T14:49:00+07:00@iana:Asia/Bangkok".
    /// </para>
    /// </remarks>
    public static class NodaXmlConvert
    {
        static readonly LocalDatePattern LocalDateRoundtrip = LocalDatePattern.CreateWithInvariantCulture("c':'yyyy'-'MM'-'dd");
        static readonly LocalDateTimePattern LocalDateTimeRoundtrip = LocalDateTimePattern.CreateWithInvariantCulture("c':'yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFF");
        static readonly OffsetPattern OffsetRoundtrip = OffsetPattern.CreateWithInvariantCulture("Z+HH:mm");

        /// <summary>
        ///   Converts a string to its <see cref="Instant"/> equivalent.
        /// </summary>
        /// <param name="s">A <see cref="string"/> in the <see cref="InstantPattern.ExtendedIsoPattern"/>, such as "2012-10-31T07:49:00Z".</param>
        /// <returns>The <see cref="Instant"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="Instant"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="instant">
        ///    &lt;xs:restriction base="xs:dateTime">
        ///      &lt;xs:pattern value="\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)Z"/>
        ///    &lt;/xs:restriction>   
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static Instant ToInstant(string s)
        {
            try
            {
                return InstantPattern.ExtendedIsoPattern.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(Instant).FullName), e);
            }
        }

        /// <summary>
        ///   Converts an <see cref="Instant"/> to a string.
        /// </summary>
        /// <param name="instant">The <see cref="Instant"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="instant"/> in the <see cref="InstantPattern.ExtendedIsoPattern"/>.</returns>
        public static string ToString(Instant instant)
        {
            return InstantPattern.ExtendedIsoPattern.Format(instant);
        }

        /// <summary>
        ///   Converts a string to its <see cref="LocalDate"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the <see cref="LocalDatePattern.IsoPattern"/>, such as "2012-10-31".
        /// </param>
        /// <returns>
        ///   The <see cref="LocalDate"/> equivalent of the <see cref="string"/>.
        /// </returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="LocalDate"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="localDate">
        ///    &lt;xs:restriction base="xs:date">
        ///      &lt;xs:pattern value="\d{4}-\d{2}-\d{2}"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static LocalDate ToLocalDate(string s)
        {
            try
            {
                return LocalDatePattern.IsoPattern.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(LocalDate).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="LocalDate"/> to a string.
        /// </summary>
        /// <param name="localDate">The <see cref="LocalDate"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="localDate"/> in the <see cref="LocalDatePattern.IsoPattern"/>.</returns>
        /// <remarks>
        ///   XML is based on ISO 8601 for dates and times and 8601 only supports the <see cref="NodaTime.CalendarSystem.Iso">Gregorian calendar</see>. 
        ///   Local dates in a different calendar system are not serialisable.
        /// </remarks>
        /// <exception cref="ArgumentException">The <see cref="LocalDate.Calendar"/> must be <see cref="NodaTime.CalendarSystem.Iso"/>.</exception>
        public static string ToString(LocalDate localDate)
        {
            Preconditions.CheckNotNull(localDate.Calendar, "localDate.Calendar");
            Preconditions.CheckArgument(
                localDate.Calendar == CalendarSystem.Iso,
                "localDate.Calendar",
                string.Format(Properties.Messages.Xml_OnlyIsoCalendars, typeof(LocalDate).FullName));

            return LocalDatePattern.IsoPattern.Format(localDate);
        }

        /// <summary>
        ///   Converts a relaxed string to its <see cref="LocalDate"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the form "c':'yyyy'-'MM'-'dd", such as "ISO:2012-10-31".
        /// </param>
        /// <returns>
        ///   The <see cref="LocalDate"/> equivalent of the <see cref="string"/>.
        /// </returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="LocalDate"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="localDateRelaxed">
        ///    &lt;xs:restriction base="xs:string">
        ///      &lt;xs:pattern value=".+:\d{4}-\d{2}-\d{2}"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static LocalDate ToLocalDateRelaxed(string s)
        {
            try
            {
                return LocalDateRoundtrip.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(LocalDate).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="LocalDate"/> to a relaxed XML string.
        /// </summary>
        /// <param name="localDate">The <see cref="LocalDate"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="localDate"/> in the form "c':'yyyy'-'MM'-'dd".</returns>
        /// <remarks>
        ///   This is not an allowable ISO 8601 form.
        /// </remarks>
        /// <exception cref="ArgumentNullException">The <see cref="LocalDate.Calendar"/> is <b>null</b>.</exception>
        public static string ToStringRelaxed(LocalDate localDate)
        {
            Preconditions.CheckNotNull(localDate.Calendar, "localDate.Calendar");

            return LocalDateRoundtrip.Format(localDate);
        }

        /// <summary>
        ///   Converts a relaxed string to its <see cref="LocalDateTime"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the form "c':'yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFF", such as "ISO:2012-10-31T10:30:00.1".
        /// </param>
        /// <returns>
        ///   The <see cref="LocalDateTime"/> equivalent of the <see cref="string"/>.
        /// </returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="LocalDateTime"/> is:
        /// <code lang="xml">  
        ///  &lt;xs:simpleType name="localDateTimeRelaxed">
        ///    &lt;xs:restriction base="xs:string">
        ///      &lt;xs:pattern value=".+:\d{4}-\d{2}-\d{2}"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static LocalDateTime ToLocalDateTimeRelaxed(string s)
        {
            try
            {
                return LocalDateTimeRoundtrip.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(LocalDateTime).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="LocalDateTime"/> to a relaxed XML string.
        /// </summary>
        /// <param name="localDateTime">The <see cref="LocalDateTime"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="localDateTime"/> in the form "c':'yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFF".</returns>
        /// <remarks>
        ///   This is not an allowable ISO 8601 form.
        /// </remarks>
        /// <exception cref="ArgumentNullException">The <see cref="LocalDateTime.Calendar"/> is <b>null</b>.</exception>
        public static string ToStringRelaxed(LocalDateTime localDateTime)
        {
            Preconditions.CheckNotNull(localDateTime.Calendar, "localDateTime.Calendar");

            return LocalDateTimeRoundtrip.Format(localDateTime);
        }

        /// <summary>
        ///   Converts a string to its <see cref="LocalDateTime"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the <see cref="LocalDateTimePattern.ExtendedIsoPattern"/>, such as "2012-10-31T14:49:00".
        /// </param>
        /// <returns>
        ///   The <see cref="LocalDateTime"/> equivalent of the <see cref="string"/>.
        /// </returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="LocalDateTime"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="localDateTime">
        ///    &lt;xs:restriction base="xs:dateTime">
        ///      &lt;xs:pattern value="\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static LocalDateTime ToLocalDateTime(string s)
        {
            try
            {
                return LocalDateTimePattern.ExtendedIsoPattern.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(LocalDateTime).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="LocalDateTime"/> to a string.
        /// </summary>
        /// <param name="localDateTime">The <see cref="LocalDateTime"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="localDateTime"/> in the <see cref="LocalDateTimePattern.ExtendedIsoPattern"/>.</returns>
        /// <remarks>
        ///   XML is based on ISO 8601 for dates and times and 8601 only supports the <see cref="NodaTime.CalendarSystem.Iso">Gregorian calendar</see>. 
        ///   Local dates in a different calendar system are not serialisable.
        /// </remarks>
        /// <exception cref="ArgumentException">The <see cref="LocalDateTime.Calendar"/> must be <see cref="NodaTime.CalendarSystem.Iso"/>.</exception>
        public static string ToString(LocalDateTime localDateTime)
        {
            Preconditions.CheckNotNull(localDateTime.Calendar, "localDateTime.Calendar");
            Preconditions.CheckArgument(
                localDateTime.Calendar == CalendarSystem.Iso,
                "localDateTime.Calendar",
                string.Format(Properties.Messages.Xml_OnlyIsoCalendars, typeof(LocalDateTime).FullName)); // TODO: Support non-ISO Calendars

            return LocalDateTimePattern.ExtendedIsoPattern.Format(localDateTime);
        }

        /// <summary>
        ///   Converts a string to its <see cref="LocalTime"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the <see cref="LocalTimePattern.ExtendedIsoPattern"/>, such as "14:49:00".
        /// </param>
        /// <returns>The <see cref="LocalTime"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="LocalTime"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="localTime">
        ///    &lt;xs:restriction base="xs:time">
        ///      &lt;xs:pattern value="\d{2}:\d{2}:\d{2}(\.\d+)"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static LocalTime ToLocalTime(string s)  // TODO: Check that seconds is only 2 digits (can we have fractions?)
        {
            try
            {
                return LocalTimePattern.ExtendedIsoPattern.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(LocalTime).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="LocalTime"/> to a string.
        /// </summary>
        /// <param name="localTime">The <see cref="LocalTime"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="localTime"/> in the <see cref="LocalTimePattern.ExtendedIsoPattern"/>.</returns>
        public static string ToString(LocalTime localTime)
        {
            return LocalTimePattern.ExtendedIsoPattern.Format(localTime);
        }

        /// <summary>
        ///   Converts a string to its <see cref="Duration"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the <see cref="PeriodPattern.NormalizingIsoPattern"/>, such as "PT2H".
        /// </param>
        /// <returns>
        ///   The <see cref="Duration"/> equivalent of the <see cref="string"/>.
        /// </returns>
        /// <exception cref="FormatException">
        ///   <paramref name="s"/> is not in a valid format or contains a <see cref="Period.Years"/> or
        ///   <see cref="Period.Months"/> component.
        /// </exception>
        /// <remarks>
        ///   The <paramref name="s"/> is first <see cref="NodaXmlConvert.ToPeriod">parsed as a period</see> and then 
        ///   <see cref="Period.ToDuration">converted</see> to a <see cref="Duration"/>.
        /// <para>
        ///   The schema definition for <see cref="Duration"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="duration">
        ///    &lt;xs:restriction base="noda:period">
        ///      &lt;xs:pattern value='[^YM]*T.*|[^YMT]+'/>  &lt;!-- Cannot contain Year or Month. -->
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static Duration ToDuration(string s)
        {
            try
            {
                return NodaXmlConvert.ToPeriod(s).ToDuration();
            }
            catch (InvalidOperationException e) // Month or Year is non-zero.
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(Duration).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="Duration"/> to a string.
        /// </summary>
        /// <param name="duration">The <see cref="Duration"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="duration"/> in the <see cref="PeriodPattern.NormalizingIsoPattern"/>.</returns>
        /// <remarks>
        ///   The <paramref name="duration"/> is first <see cref="Period.ToDuration">converted</see> to a <see cref="Period"/>
        ///   and <see cref="NodaXmlConvert.ToString(Period)">formatted as a period</see>.
        /// </remarks>
        public static string ToString(Duration duration)
        {
            return NodaXmlConvert.ToString(duration.ToPeriod());
        }

        /// <summary>
        ///   Converts a string to its <see cref="Period"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the <see cref="PeriodPattern.NormalizingXmlPattern"/>, such as "P3M2DT1H".
        /// </param>
        /// <returns>The <see cref="Period"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   When <paramref name="s"/> is <see cref="string.IsNullOrEmpty"/> then <see cref="Period.Zero"/> is returned.
        /// </para>
        /// <para>
        ///   The schema definition for <see cref="Period"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="period">
        ///    &lt;xs:restriction base="xs:duration">
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static Period ToPeriod(string s)
        {
            try
            {
                if (string.IsNullOrEmpty(s))
                    return Period.Zero;

                return PeriodPattern.NormalizingXmlPattern.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(Period).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="Period"/> to a string.
        /// </summary>
        /// <param name="period">The <see cref="Period"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="period"/> in the <see cref="PeriodPattern.NormalizingIsoPattern"/>.</returns>
        /// <exception cref="ArgumentException">
        ///   When the components of the <paramref name="period"/> are both positive and negative.  This case is not allowed by XML and ISO 8601.
        /// </exception>
        /// <seealso cref="ToStringRelaxed(Period)"/>
        public static string ToString(Period period)
        {
            return PeriodPattern.NormalizingXmlPattern.Format(period ?? Period.Zero);
        }

        /// <summary>
        ///   Converts a <see cref="Period"/> to a string.
        /// </summary>
        /// <param name="period">The <see cref="Period"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="period"/> in the <see cref="PeriodPattern.RoundtripPattern"/>.</returns>
        /// <remarks>
        ///   Produces XML output that is not compatible with the XML duration type when a component is negative.
        /// </remarks>
        /// <seealso cref="ToString(Period)"/>
        public static string ToStringRelaxed(Period period)
        {
            return PeriodPattern.RoundtripPattern.Format(period ?? Period.Zero);
        }

        /// <summary>
        ///   Converts a string to its <see cref="Period"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the <see cref="PeriodPattern.RoundtripPattern"/>, such as "P3M2DT-1H".
        /// </param>
        /// <returns>The <see cref="Period"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   When <paramref name="s"/> is <see cref="string.IsNullOrEmpty"/> then <see cref="Period.Zero"/> is returned.
        /// </para>
        /// </remarks>
        public static Period ToPeriodRelaxed(string s)
        {
            try
            {
                if (string.IsNullOrEmpty(s))
                    return Period.Zero;

                return PeriodPattern.RoundtripPattern.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(Period).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a string to its <see cref="Offset"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the form "Z+HH:mm", such as "+07:00".
        /// </param>
        /// <returns>The <see cref="Offset"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="Offset"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="offset">
        ///    &lt;xs:restriction base="xs:string">
        ///      &lt;xs:pattern value="Z|[+-]\d{2}:\d{2}"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static Offset ToOffset(string s)
        {
            try
            {
                return OffsetRoundtrip.Parse(s).Value;
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(Offset).FullName), e);
            }
        }

        /// <summary>
        ///   Converts an <see cref="Offset"/> to a string.
        /// </summary>
        /// <param name="offset">The <see cref="Offset"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="offset"/> in the form "Z+HH:mm".</returns>
        public static string ToString(Offset offset)
        {
            return OffsetRoundtrip.Format(offset);
        }

        /// <summary>
        ///   Converts a string to its <see cref="OffsetDateTime"/> equivalent.
        /// </summary>
        /// <param name="s">A <see cref="string"/> in the form
        ///   "<see cref="LocalDateTimePattern.ExtendedIsoPattern"/> then 'Z+HH:mm'",
        ///   such as "2012-10-31T14:49:00+07:00".
        /// </param>
        /// <returns>The <see cref="OffsetDateTime"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="OffsetDateTime"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="offsetDateTime">
        ///    &lt;xs:restriction base="xs:dateTime">
        ///      &lt;xs:pattern value="\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)(Z|[+-]\d{2}:\d{2})"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static OffsetDateTime ToOffsetDateTime(string s)
        {
            try
            {
                var tzIndex = s.LastIndexOfAny(new char[] { '+', '-' });
                if (tzIndex < 0)
                    throw new FormatException("The OffsetDateTime value is missing the offset.");
                var dateTimeText = s.Substring(0, tzIndex);
                var offsetText = s.Substring(tzIndex);
                return new OffsetDateTime(ToLocalDateTime(dateTimeText), ToOffset(offsetText));
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(OffsetDateTime).FullName), e);
            }
        }

        /// <summary>
        ///   Converts an <see cref="OffsetDateTime"/> to a string.
        /// </summary>
        /// <param name="value">The <see cref="OffsetDateTime"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="value"/> in the 
        ///   <see cref="LocalDateTimePattern.ExtendedIsoPattern"/> followed by the "Z+HH:mm".
        /// </returns>
        /// <remarks>
        ///   XML is based on ISO 8601 for dates and times and 8601 only supports the <see cref="NodaTime.CalendarSystem.Iso">Gregorian calendar</see>. 
        ///   Local dates in a different calendar system are not serialisable.
        /// </remarks>
        /// <exception cref="ArgumentException">The <see cref="LocalDateTime.Calendar"/> must be <see cref="NodaTime.CalendarSystem.Iso"/>.</exception>
        public static string ToString(OffsetDateTime value)
        {
            return ToString(value.LocalDateTime) + ToString(value.Offset);
        }

        /// <summary>
        ///   Converts a string to its <see cref="DateTimeZone"/> equivalent.
        /// </summary>
        /// <param name="id">
        ///   A <see cref="string"/> containing the <see cref="DateTimeZone.ProviderId"/> and <see cref="DateTimeZone.Id">DateTimeZone.Id</see>, 
        ///   such as "iana:Asia/Bangkok".
        /// </param>
        /// <returns>
        ///   The <see cref="DateTimeZone"/> with the specified <paramref name="id"/>
        ///   or <b>null</b> if <paramref name="id"/> is null or empty.</returns>
        /// <exception cref="FormatException">
        ///   <paramref name="id"/> can not be found or is not unique.
        /// </exception>
        /// <remarks>
        ///   The <see cref="DateTimeZoneProviders.All">DateTimeZoneProviders.All</see> property is used to map the <paramref name="zoneId"/> to a specific 
        ///  <see cref="DateTimeZone"/> instance.
        /// <para>
        ///   ISO 8601 is completely silent on named time zones; and no other international standard addresses the issue. 
        ///   It is <b>assumed</b> that different providers will use unique time zone ids, except for <see cref="DateTimeZone.UtcId">UTC</see>.
        ///   In the future, maybe we should not care about uniqueness.
        /// </para>
        /// <para>
        ///   The schema definition for <see cref="DateTimeZone"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="dateTimeZone">
        ///    &lt;xs:restriction base="xs:string">
        ///      &lt;xs:whiteSpace value="collapse"/>
        ///      &lt;xs:pattern value=".+:.+"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static DateTimeZone ToDateTimeZone(string id)
        {
            if (string.IsNullOrEmpty(id))
                return null;

            var parts = id.Split(':');
            if (parts.Length != 2)
                throw new FormatException(string.Format(Properties.Messages.Xml_DateTimeZoneNotFound, id));

            try
            {
                return DateTimeZoneProviders.ForId(parts[0])[parts[1]];
            }
            catch (Exception e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_DateTimeZoneNotFound, id), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="DateTimeZone"/> to a string.
        /// </summary>
        /// <param name="zone">The <see cref="DateTimeZone"/> to convert.</param>
        /// <returns>A <see cref="string"/> containing the <see cref="DateTimeZone.Id"/> or <b>string.Empty</b> if <paramref name="zone"/> is <b>null</b>.</returns>
        public static string ToString(DateTimeZone zone)
        {
            if (zone == null)
                return string.Empty;

            return zone.ProviderId + ":" + zone.Id;
        }

        /// <summary>
        ///   Converts a string to its <see cref="RecurrenceRule"/> equivalent.
        /// </summary>
        /// <param name="s">A RFC 5545 "RRULE" <see cref="string"/>.</param>
        /// <returns>The <see cref="RecurrenceRule"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        public static RecurrenceRule ToRecurrenceRule(string s)
        {
            return RecurrenceRule.Parse(s);
        }

        /// <summary>
        ///   Converts a <see cref="RecurrenceRule"/> to a string.
        /// </summary>
        /// <param name="rule">The <see cref="RecurrenceRule"/> to convert.</param>
        /// <returns>The RFC 5545 representation of the <paramref name="rule"/>.</returns>
        public static string ToString(RecurrenceRule rule)
        {
            return rule.ToString();
        }

        /// <summary>
        ///   Converts a string to its <see cref="Interval"/> equivalent.
        /// </summary>
        /// <param name="s">
        ///   A <see cref="string"/> in the form "<see cref="InstantPattern.ExtendedIsoPattern"/> '/' <see cref="InstantPattern.ExtendedIsoPattern"/>",
        ///   such as "2012-10-31T07:49:00Z/2012-10-31T09:49:00Z".
        /// </param>
        /// <returns>
        ///   The <see cref="Interval"/> equivalent of the <see cref="string"/>.
        /// </returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        /// <remarks>
        /// <para>
        ///   The schema definition for <see cref="Interval"/> is:
        /// <code lang="xml">
        ///  &lt;xs:simpleType name="interval">
        ///    &lt;xs:restriction base="xs:string">
        ///      &lt;xs:pattern value="\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)Z/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)Z"/>
        ///    &lt;/xs:restriction>
        ///  &lt;/xs:simpleType>
        /// </code>
        /// </para>
        /// </remarks>
        public static Interval ToInterval(string s)
        {
            try
            {
                var parts = s.Split('/');
                if (parts.Length != 2)
                    throw new FormatException("Expected a '/' to separate the start and end dates.");
                return new Interval(NodaXmlConvert.ToInstant(parts[0]), NodaXmlConvert.ToInstant(parts[1]));
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(Interval).FullName), e);
            }
        }

        /// <summary>
        ///   Converts an <see cref="Interval"/> to a string.
        /// </summary>
        /// <param name="interval">The <see cref="Interval"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="interval"/> in the TODO pattern.</returns>
        public static string ToString(Interval interval)
        {
            return NodaXmlConvert.ToString(interval.Start) + "/" + NodaXmlConvert.ToString(interval.End);
        }

        /// <summary>
        ///   Converts a string to its <see cref="ZonedDateTime"/> equivalent.
        /// </summary>
        /// <param name="s">A <see cref="string"/> in the TODO pattern.</param>
        /// <returns>The <see cref="ZonedDateTime"/> equivalent of the <see cref="string"/>.</returns>
        /// <exception cref="FormatException"><paramref name="s"/> is not in a valid format.</exception>
        public static ZonedDateTime ToZonedDateTime(string s)
        {
            try
            {
                var parts = s.Split('@');
                if (parts.Length != 2)
                    throw new FormatException("Expected a '@' to separate the offset date time and time zone.");
                var offsetDateTime = NodaXmlConvert.ToOffsetDateTime(parts[0]);
                var zone = NodaXmlConvert.ToDateTimeZone(parts[1]);
                return new ZonedDateTime(offsetDateTime.LocalDateTime, offsetDateTime.Offset, zone);
            }
            catch (UnparsableValueException e)
            {
                throw new FormatException(string.Format(Properties.Messages.Xml_InvalidValueForType, s, typeof(ZonedDateTime).FullName), e);
            }
        }

        /// <summary>
        ///   Converts a <see cref="ZonedDateTime"/> to a string.
        /// </summary>
        /// <param name="zonedDateTime">The <see cref="ZonedDateTime"/> to convert.</param>
        /// <returns>A <see cref="string"/> representation of the <paramref name="zonedDateTime"/> in the TODO pattern.</returns>
        public static string ToString(ZonedDateTime zonedDateTime)
        {
            return NodaXmlConvert.ToString(zonedDateTime.ToOffsetDateTime()) + "@" + NodaXmlConvert.ToString(zonedDateTime.Zone);
        }

    }
}
